package main

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path"
	"path/filepath"
	"strings"
	"unicode"

	"gitee.com/geektime-geekbang/geektime-go/advance/template/gen/annotation"
	"gitee.com/geektime-geekbang/geektime-go/advance/template/gen/http"
)

// 实际上 main 函数这里要考虑接收参数
// src 源目标
// dst 目标目录
// type src 里面可能有很多类型，那么用户可能需要指定具体的类型
// 这里我们简化操作，只读取当前目录下的数据，并且扫描下面的所有源文件，然后生成代码
// 在当前目录下运行 go install 就将 main 安装成功了，
// 可以在命令行中运行 gen
// 在 testdata 里面运行 gen，则会生成能够通过所有测试的代码
func main() {
	err := gen(".")
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	fmt.Println("success")
}

func gen(src string) error {
	// 第一步找出符合条件的文件
	srcFiles, err := scanFiles(src)
	if err != nil {
		return err
	}
	// 第二步，AST 解析源代码文件，拿到 service definition 定义
	defs, err := parseFiles(srcFiles)
	if err != nil {
		return err
	}
	// 生成代码
	return genFiles(src, defs)
}

// 根据 defs 来生成代码
// src 是源代码所在目录，在测试里面它是 ./testdata
func genFiles(src string, defs []http.ServiceDefinition) error {
	for _, def := range defs {
		bs := &bytes.Buffer{}
		err := http.Gen(bs, def)
		file, err := os.OpenFile(path.Join(src, underscoreName(def.Name)+"_gen.go"), os.O_WRONLY|os.O_CREATE, 0666)

		if err != nil {
			return err
		}
		defer file.Close()

		if err != nil {
			fmt.Printf("open file error=%v\n", err)
			return err
		}
		writer := bufio.NewWriter(file)
		writer.Write(bs.Bytes())
		writer.Flush()
	}
	return nil
}

func parseFiles(srcFiles []string) ([]http.ServiceDefinition, error) {
	defs := make([]http.ServiceDefinition, 0, 20)
	for _, src := range srcFiles {
		fmt.Println(src)
		// 你需要利用 annotation 里面的东西来扫描 src，然后生成 file
		fset := token.NewFileSet()
		f, err := parser.ParseFile(fset, src, nil, parser.ParseComments)
		if err != nil {
			return nil, err
		}
		sfev := &annotation.SingleFileEntryVisitor{}
		ast.Walk(sfev, f)

		file := sfev.Get()

		for _, typ := range file.Types {
			_, ok := typ.Annotations.Get("HttpClient")
			if !ok {
				continue
			}
			def, err := parseServiceDefinition(file.Node.Name.Name, typ)
			if err != nil {
				return nil, err
			}
			defs = append(defs, def)
		}
	}
	return defs, nil
}

// 你需要利用 typ 来构造一个 http.ServiceDefinition
// 注意你可能需要检测用户的定义是否符合你的预期
func parseServiceDefinition(pkg string, typ annotation.Type) (http.ServiceDefinition, error) {
	result := &http.ServiceDefinition{
		Package: pkg,
	}
	for _, a := range typ.Annotations.Ans {
		if a.Key == "ServiceName" {
			result.Name = a.Value
		}
	}
	if result.Name == "" {
		result.Name = typ.Annotations.Node.Name.Name
	}
	method := &http.ServiceMethod{}
	fields := typ.Fields
	for _, field := range fields {
		method.Name = field.Annotations.Node.Names[0].Name

		mAns := field.Annotations.Ans
		for _, ma := range mAns {
			if ma.Key == "Path" {
				method.Path = ma.Value
			}
		}
		if method.Path == "" {
			method.Path = "/" + method.Name
		}
		params := field.Annotations.Node.Type.(*ast.FuncType).Params
		if len(params.List) != 2 {
			return *result, errors.New("gen: 方法必须接收两个参数，其中第一个参数是 context.Context，第二个参数请求")
		}
		method.ReqTypeName = params.List[1].Type.(*ast.StarExpr).X.(*ast.Ident).Name
		results := field.Annotations.Node.Type.(*ast.FuncType).Results
		if len(results.List) != 2 {
			return *result, errors.New("gen: 方法必须返回两个参数，其中第一个返回值是响应，第二个返回值是error")
		}
		method.RespTypeName = results.List[0].Type.(*ast.StarExpr).X.(*ast.Ident).Name
		result.Methods = append(result.Methods, *method)
	}
	return *result, nil
}

// 返回符合条件的 Go 源代码文件，也就是你要用 AST 来分析这些文件的代码
func scanFiles(src string) ([]string, error) {
	srcFiles := make([]string, 0, 10)
	files, err := os.ReadDir(src)
	if err != nil {
		return nil, err
	}
	for _, file := range files {
		if strings.HasSuffix(file.Name(), ".go") &&
			!strings.HasSuffix(file.Name(), "_test.go") &&
			!strings.HasSuffix(file.Name(), "gen.go") {
			src, err = filepath.Abs(src)
			if err != nil {
				return nil, err
			}
			srcFiles = append(srcFiles, filepath.Join(src, file.Name()))
		}
	}
	return srcFiles, nil
	//srcAbs, err := filepath.Abs(src)
	//if err != nil {
	//	return nil, err
	//}
	//files := make([]string, 0)
	//if err := filepath.Walk(src, func(filePath string, f os.FileInfo, err error) error {
	//	if f == nil {
	//		return err
	//	}
	//	if f.IsDir() {
	//		return nil
	//	}
	//	if strings.HasSuffix(f.Name(), ".go") &&
	//		!strings.HasSuffix(f.Name(), "_test.go") &&
	//		!strings.HasSuffix(f.Name(), "gen.go") {
	//		files = append(files, path.Join(srcAbs, filePath))
	//	}
	//	return nil
	//}); err != nil {
	//	return nil, err
	//}
	//return files, nil
}

// underscoreName 驼峰转字符串命名，在决定生成的文件名的时候需要这个方法
// 可以用正则表达式，然而我写不出来，我是正则渣
func underscoreName(name string) string {
	var buf []byte
	for i, v := range name {
		if unicode.IsUpper(v) {
			if i != 0 {
				buf = append(buf, '_')
			}
			buf = append(buf, byte(unicode.ToLower(v)))
		} else {
			buf = append(buf, byte(v))
		}

	}
	return string(buf)
}
